package org.ChrisRiffle.Acme.BoundingBox.controlers;

import java.awt.geom.Point2D;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import org.ChrisRiffle.Acme.BoundingBox.model.DataPoint;


/**
 * Controller object handles controlling operations dealing 
 * directly with the DataSet of points
 * 
 * Recommended to calling its init method first, instead of just querying
 * immediately(which would have to also perform the initialization)
 * 
 * @author RIF
 */
public enum DataSetController {
    
	//singleton
    INSTANCE;

    
    /**
     * X position in a csv line
     */
	private static final int X_CSV_POS = 0;
	
	
    /**
     * Y position in a csv line
     */	
	private static final int Y_CSV_POS = 1;
	
	
    /**
     * 'value' position in a csv line
     */
	private static final int VALUE_CSV_POS = 2;


	/**
	 *  Note: this must be reachable on the classpath
	 */
    private static final String CSV_INPUT_FILE_PATH = "res/sample_data.csv";    

    
    /**
     * Intended to be the basic unordered list of all the points parsed from the csv file
     */
    private List<DataPoint> dataSetPointList;
                
    
    /**
     * Each first map key("x") will map to another map that contains "y" keys
     * 
     * Each "y" key in the y map will map to a list of dataPoint objects
     * 
     * The keys (x and y) coincide with the "floor whole number integer" portion of the 
     * float for the point coordinates being stored in the list associated with those keys
     * 
     * Example:  point (10.5, 200.35) will be stored using the "x map" x index of "10" and 
     * by using the "Y" map index of "200"
     * 
     * It is hard coded to base these whole numbers on the multiple of 10 (ten)
     */
    private Map<Integer, Map<Integer, List<DataPoint>>> dataSetSpatialIndex;
        
    
    /**
     * Represents the CSV file on disk that contains all the points
     */
    private InputStream csvInputStream;
    
    
    
	/**
	 * 
	 * Initializes this controller object by reading csv points off disk.
	 * 
	 * Recommended: to call this method Before querying points, with programmer
	 * coordination so the points can just be queried quickly when needed
	 * without having to initialize.
	 * 
	 * This method should only be called Once - at app initialization.
	 * 
	 * It is a very expensive $$ method to call
	 */
	public void init() {
		
		Logger.getAnonymousLogger().info("Initializing the DataSet Controller");
		
		try {
			
			// no need to capture this list for now, just call this 
			// to get them stored in this controler object's local 
			// variable
			getDataSetPointList();
			
			// no need to capture this now, but just call it so it creates 
			// our spatial index and caches it into a local variable
			getDataSetSpatialIndex();
			
			Logger.getAnonymousLogger().info("DONE initializing the DataSet Controller");
			
		} catch (NumberFormatException e) {

			e.printStackTrace();
			
		} catch (URISyntaxException e) {

			e.printStackTrace();
			
		} catch (IOException e) {

			e.printStackTrace();
		}

	}
    
    
	/**
	 * 
	 * Queries the DataSet for points contained by the region specified by the 
	 * parameter points in this method.  It is inclusive - DataSet points lying 
	 * on the line of the specified region will be returned.  The specifed region 
	 * is a rectangle.  
	 * 
	 * The top left point of the desired rectangle region to query is located 
	 * at location (0,0), (for x and y)
	 * 
	 * The bottom right point of the desired rectangle region to query should have 
	 * values for its x and y that are Greater than the x/y values of the top left 
	 * corner point.  Values increase as one moves to the right and down.  Values decrease 
	 * as one moves to the top and to the left 
	 * 
	 * No x or y values are allowed to be negative, they must be positive floats.  
	 * 
	 * Note: It is recommended to initialize this controller class First at the programmers
	 * best-juged appropriate time, before querying for these points.  That will allow the 
	 * query to just get the points from its internal collections, without first having to read 
	 * and parse all the points from the csv file on disk
	 * 
	 * 
	 * @param pTopLeftPoint
	 * 				A point representing the top left corner of a rectangle
	 * 
	 * 
	 * @param pBottomRightPoint
	 * 				A point representing the bottom right corner of a rectangle
	 * 
	 * @return
	 * 		A list of all the {@link DataPoint} objects contained by the rectangular region 
	 * 		specified by the parameters specified in this method
	 * 
	 */
	public List<DataPoint> queryPoints(Point2D.Float pTopLeftPoint, Point2D.Float pBottomRightPoint){
		
		List<DataPoint> potentialPointList = null;
				
		try {
			
			potentialPointList = getPotentialMatchingPointList(
					pTopLeftPoint, pBottomRightPoint);
			
			
		} catch (NumberFormatException e) {
	
			e.printStackTrace();
			return null;
			
		} catch (URISyntaxException e) {
	
			e.printStackTrace();
			return null;
			
		} catch (IOException e) {
	
			e.printStackTrace();
			return null;
		}
		
		
		List<DataPoint> matchingPointList = getMatchingPointList(potentialPointList, pTopLeftPoint, pBottomRightPoint);
				
	  return matchingPointList;
	}


	/**
	 * 
	 * Writes specified points to the specified csv file
	 * 
	 * This method expects to be given a valid file that exists.
	 * 
	 * @param pPointList
	 * 				The list of points to save
	 * 
	 * @param pCsvFileToWrite
	 * 				The csv file to write to
	 * 
	 * @throws IOException 
	 * 				If the points couldn't be written to the file
	 * 				
	 */
	public void writePointsToCsvFile(List<DataPoint> pPointList, File pCsvFileToWrite) 
		throws IOException{
	
		
		if (pCsvFileToWrite.exists()) {
			
			//start fresh with an empty file
			pCsvFileToWrite.delete();
		}
	
		//create the file incase it didn't exist
		pCsvFileToWrite.createNewFile();
	
		BufferedWriter bW = new BufferedWriter(new FileWriter(pCsvFileToWrite));
	
		// to be reused in the loop
		String lineToWrite = "";
	
		for (DataPoint dp : pPointList) {
	
			// make a string of comma separated values from the point objects fields
			// in the order of: x,y,value
			lineToWrite = dp.getX() + "," + dp.getY() + "," + dp.getValue();
	
			bW.write(lineToWrite);
			bW.newLine();
		}
		
		// free the IO resources
		bW.close();
		
	
	}


	/**
	 * 
	 * Gets the Internal CSV file usint the classpath
	 * 
	 * Note: depends on the toplevel folder of the app(build.xml or the bin jar)
	 * being included on the classpath 
	 * 
	 * @return the Internal CSV inputStream 
	 * 
	 * @throws URISyntaxException
	 *             If it can't get the file
	 * 
	 */
	private InputStream getCsvInputStream() throws URISyntaxException {

		if (csvInputStream == null) {

			
			csvInputStream = ClassLoader.getSystemResourceAsStream(CSV_INPUT_FILE_PATH);
			
			
		}
		return csvInputStream;
	}
    
    


	/**
	 * Gets a list of all the data set points - UNordered
	 * 
	 * The first time this method is called, it's expensive because it reads all
	 * the points from a csv file on disk. Succesive calls on this method should
	 * be pretty fast because the points are cached in a local variabe list so
	 * all that has to happen then is to return the list
	 * 
	 * Note: this method is very expensive $$ and should never be called from
	 * the Swing Event Dispatch Thread of a Swing app
	 * 
	 * @return An unordered list of all data points
	 * 
	 * @throws URISyntaxException
	 * 					If the points couldn't be obtained
	 * 
	 * @throws IOException
	 * 					If the points couldn't be obtained
	 * 
	 * @throws NumberFormatException
	 * 					If the points couldn't be obtained
	 */
	private List<DataPoint> getDataSetPointList()
			throws URISyntaxException, NumberFormatException, IOException {

		if (dataSetPointList == null) {
			
			Logger.getAnonymousLogger().info("Reading CSV file into DataPoint's...");

			dataSetPointList = new LinkedList<DataPoint>();

			BufferedReader br = new BufferedReader(new InputStreamReader(
					getCsvInputStream()));

			
			// raw line read in from the file
			String currentLine = "";
			
			// should always only contain 3 elements as they are read from each line
			//  i.e.:   x,y,value
			String[] csvArray;

			while ((currentLine = br.readLine()) != null) {

				//array of values from the line
				csvArray = currentLine.split(",");

				float x = Float.parseFloat(csvArray[X_CSV_POS]);

				float y = Float.parseFloat(csvArray[Y_CSV_POS]);

				float value = Float.parseFloat(csvArray[VALUE_CSV_POS]);

				DataPoint p = new DataPoint(x, y, value);

				dataSetPointList.add(p);
			}

			//free IO resources
			br.close();
			
			Logger.getAnonymousLogger().info("COMPLETE parsing .csv file");

		}
		return dataSetPointList;
	}

	
	
	/**
     * 
     * Gets the home made spatial index, that's filled with all the points from 
     * the DataSet
     * 
     * Each first map key("x") will map to another map that contains "y" keys
     * 
     * Each "y" key in the y map will map to a list of dataPoint objects
     * 
     * The keys (x and y) coincide with the "floor whole number integer" portion of the 
     * float for the point coordinates being stored in the list associated with those keys
     * 
     * Example:  point (10.5, 200.35) will be stored using the "x map" x index of "10" and 
     * by using the "Y" map index of "200"
     * 
     * It is hard coded to base these whole numbers on the multiple of 10 (ten)
     * 
     * @return
     * 		The home made spatial index filled with all the {@link DataPoint} objects in 
     * 		the DataSet
     * 
     * @throws IOException 
     * 				If the spatial index couldn't be obtained
     * 
     * @throws URISyntaxException
     * 						If the spatial index couldn't be obtained
     *  
     * @throws NumberFormatException 
     * 							If the spatial index couldn't be obtained
     *  
     */
	private Map<Integer, Map<Integer, List<DataPoint>>> getDataSetSpatialIndex() throws 
		NumberFormatException, URISyntaxException, IOException {

		if (dataSetSpatialIndex == null) {
			
			Logger.getAnonymousLogger().info("Creating the SpatialIndex of DataPoint's");

			dataSetSpatialIndex = new HashMap<Integer, Map<Integer, List<DataPoint>>>();

			for (DataPoint iDp : getDataSetPointList()) {

				// get our map index that we'll use to store the datapoint in the correct location 
				// based on the x value of that datapoint
				int xFactor10FloorBucket = floorMultiple10(iDp.getX());

				Map<Integer, List<DataPoint>> yFact10BucketToDataPointListMap = dataSetSpatialIndex
						.get(xFactor10FloorBucket);

				// if there is No "y map", then there must not have been any points stored yet for 
				// this location(until now), so we have to create the map and then store its reference 
				// in the "x map" for this x key
				if (yFact10BucketToDataPointListMap == null) {

					yFact10BucketToDataPointListMap = new HashMap<Integer, List<DataPoint>>();

					dataSetSpatialIndex.put(xFactor10FloorBucket,
							yFact10BucketToDataPointListMap);
				}

				// get our Y map index that we'll use to store the datapoint in the correct location 
				// based on the Y value of that datapoint
				int yFactor10Bucket = floorMultiple10(iDp.getY());

				List<DataPoint> dataPointList = yFact10BucketToDataPointListMap
						.get(yFactor10Bucket);

				// if there is no list of datapoints here for the current Y value, then that means 
				// this is the first datapoint being stored for this curent (x,y) combination, so 
				// we'll create the list, store its reference in our spatial index object
				if (dataPointList == null) {

					dataPointList = new ArrayList<DataPoint>();

					yFact10BucketToDataPointListMap.put(yFactor10Bucket, dataPointList);
				}

				//add the datapoint to this list which is sored in its correct location of the 
				// x/y indexs in the maps of our spatial index based on the x/y coordinates of 
				// the datapoint itself.  
				dataPointList.add(iDp);
							
			}//loop
			
			Logger.getAnonymousLogger().info("DONE creating the SpatialIndex");
		}
		return dataSetSpatialIndex;
	}
    
	
	/**
	 * 
	 * Gets Possible Points List From SpatialIndex
	 * 
	 * @param pTopLeftPoint
	 *            A point representing the top left corner of a rectangle
	 * 
	 * @param pBottomRightPoint
	 *            A point representing the bottom right corner of a rectangle
	 * 
	 * @return A list of all the datapoint objects contained by the rectangular
	 *         region specified by the parameters specified in this method
	 * 
	 * @throws IOException
	 *             If it couldn't get the points
	 * 
	 * @throws URISyntaxException
	 *             If it couldn't get the points
	 * 
	 * @throws NumberFormatException
	 *             If it couldn't get the points
	 * 
	 */
	private List<DataPoint> getPotentialMatchingPointList(Point2D.Float pTopLeftPoint, 
			Point2D.Float pBottomRightPoint) throws NumberFormatException, URISyntaxException, IOException {

		/*
		 * The points will have been placed in our spatial index based on the 
		 * rounded down floor multiple 10 of the whole value portion of their 
		 * float.  So we can use our utility method to get that "bucket index" 
		 * to locate the point
		 */		
		
		int xLeftIndex = floorMultiple10(pTopLeftPoint.x);

		int yTopIndex = floorMultiple10(pTopLeftPoint.y);

		int xRightIndex = floorMultiple10(pBottomRightPoint.x);

		int yBottomIndex = floorMultiple10(pBottomRightPoint.y);

		/*
		 * Possible point list will be of all the points in our spatial index that 
		 * have the same floor multiple of 10.
		 * 
		 * Example all points having an X value between 10.0 and 19.9 will be in our spatial 
		 * index X bucket of the index "10"
		 * 
		 * Once we get all those possible points, then we can filter out only the ones 
		 * contained by the specified rectangle region. 
		 */
		List<DataPoint> possiblePointList = new ArrayList<DataPoint>();

		
		//loop through all the "X" "multiple of 10" buckets
		for (int x = xLeftIndex; x <= xRightIndex; x += 10) {

			// loop through all the "Y" multiple of 10 buckets
			for (int y = yTopIndex; y <= yBottomIndex; y += 10) {

				// get the -- map < x bucket index TO y map >
				Map<Integer, Map<Integer, List<DataPoint>>> xBucketToYmap = getDataSetSpatialIndex();

				// get the -- map < Y bucket index TO  list<DataPoint>
				Map<Integer, List<DataPoint>> yIndexBucketToDataPointListMap = xBucketToYmap.get(x);

				
				if (yIndexBucketToDataPointListMap == null || yIndexBucketToDataPointListMap.isEmpty()) {
					
					//no points were ever stored for that x index bucket,.. so go to the next x and keep 
					// looking
					continue;
				}

				
				List<DataPoint> currentSpatialGridIndexPointList = yIndexBucketToDataPointListMap.get(y);

				if (currentSpatialGridIndexPointList == null || currentSpatialGridIndexPointList.isEmpty()) {
					
					// No points were added to this location, (rare),.. and I'm thinking maybe impossible
					continue;

				} else {

					// add all the points from this list at the location of our current cell 
					// in our grid spatial index structre - add them to our possiblePointList 
					// list which can be used to refine further later to get the exact matches
					possiblePointList.addAll(currentSpatialGridIndexPointList);
				}
			}
			
		}
		
		
		return possiblePointList; 
	}

	
	
	
	/**
	 * 
	 * Looks at the specified parameter of the possible point list, and then
	 * applies more conditions to arrive at the final correct contained points
	 * 
	 * @param pPossiblePointList
	 *            A list of the possible points from which the exact points will
	 *            be returned
	 *            
	 * @param pTopLeftPoint
	 * 				The top left point
	 *  
	 * @param pBottomRightPoint
	 * 				The bottom right point
	 * 
	 * @return The exact points contained by the specified region
	 * 
	 */
	private List<DataPoint> getMatchingPointList(List<DataPoint> pPossiblePointList, 
			Point2D.Float pTopLeftPoint, Point2D.Float pBottomRightPoint) {
				
		List<DataPoint> rBoxBoundedPointList = new ArrayList<DataPoint>();
	
		// filter out the potential points that aren't in the rectangle
		// only add points to a list if those points are in the user-specified
		// rectangle
		for (DataPoint p : pPossiblePointList) {
	
			boolean bContainsX = p.getX() >= pTopLeftPoint.getX()
					&& p.getX() <= pBottomRightPoint.getX();
	
			boolean bContainsY = p.getY() >= pTopLeftPoint.getY()
					&& p.getY() <= pBottomRightPoint.getY();
	
			if (bContainsX && bContainsY) {
	
				rBoxBoundedPointList.add(p);
			}
		}
					
		return rBoxBoundedPointList;
	}


	/**
	 * Rounds the specified float down to the nearest whole number that is a
	 * multiple of 10
	 * 
	 * @param pFloat
	 *            The number to round down
	 * @return 
	 * 		The nearest whole number that is a multiple of 10
	 */
	private static int floorMultiple10(float pFloat) {
	
		int floorInt = (int) Math.floor(pFloat);
	
		if (floorInt % 10 == 0) {
	
			// it's already a multiple of 10
			return floorInt;
		}
	
		// return the closest bottom/floor multiple of 10(round down)
		return (floorInt / 10) * 10;
	}
    
    
  
}


